/**
 * Copyright 2009 cestum.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cestum.cave.presenter.rpc
{	
	import com.google.protobuf.Builder;
	import com.google.protobuf.CodedInputStream;
	import com.google.protobuf.CodedOutputStream;
	import com.google.protobuf.Message;
	import com.hurlant.math.BigInteger;
	
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.Socket;
	import flash.utils.ByteArray;
	
	import org.cestum.cave.logger.Logger;
	import org.cestum.cave.ui.events.CaveSocketConnEvent;
	import org.goverla.collections.HashMap;


	/**
	 * Wave specific client channel.
	 * Sends, Receives raw data
	 * Builds the incoming raw data with protobuf
	 */
	public class CaveSocket extends Socket
	{
		private var expectedMessages:HashMap;
		private var outputStream:CodedOutputStream;
		private var requiredSize:int = -1;
		private var lastReadByte:int = -1;
		private var buffer:ByteArray ;
		private var connectionErr:String;
		private var secConnectionErr:String;
		private var dataCompleteEvent:Event = new Event("Data_Buffered");
		private var inputBuffer:ByteArray;
		private var callback:ISocketDataCallBack;
		
		public function CaveSocket(_callback:ISocketDataCallBack, host:String=null, port:int=9876)
		{
			super(host, port);		
			this.callback = _callback;
			addEventListener(Event.CONNECT, onConnect);		
			addEventListener(Event.CLOSE, onClose);
			addEventListener(IOErrorEvent.IO_ERROR, onError);		
			addEventListener(ProgressEvent.SOCKET_DATA, onResponse);
			addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
			addEventListener("DataComplete", onCompleteData);
			expectedMessages = new HashMap();
			outputStream = CodedOutputStream.newInstance(this);
			buffer = new ByteArray();
			inputBuffer = new ByteArray();
		}
		
		private function onConnect(e:Event):void {
			Logger.log("Connected");
			if (callback instanceof CaveChannel)
				(CaveChannel(callback)).generateEvent(new CaveSocketConnEvent(CaveSocketConnEvent.CONNECTED));
		}
		
		private function onClose(e:Event):void{
			Logger.log("Connection Closed.");
		}
		
		private function onError(e:IOErrorEvent):void{
			Logger.log("Connection Error: " + e.text);
		}
		
		private function securityErrorHandler(event:SecurityErrorEvent):void {
			//lets show a little info
			Logger.log("Connection Security Error: " + event.text);
		}
		
		private function onResponse(e:ProgressEvent):void{
			trace("Recieved Response");
			//		var str:String = readUTFBytes(bytesAvailable);
			if (inputBuffer.bytesAvailable == 0)
				inputBuffer.clear();
			readBytes(inputBuffer, inputBuffer.length, bytesAvailable);
			dispatchEvent(new Event("DataComplete"));
			
			//		Alert.show("b length: " + buffer.length  + "\n bytesA: " + bytesAvailable  + "\n Req: " + requiredSize);
			//		if (requiredSize == -1) {
			//			try {
			//				if (lastReadByte != -1)
			//					requiredSize = ipStream.appendReadRawLittleEndian32(lastReadByte);
			//				else
			//					requiredSize = ipStream.readRawLittleEndian32();
			//			} catch(e) {	
			//				lastReadByte = ipStream.lastReadByte;
			//				return;
			//			}
			//		}
			//if bytesAvailable <  requiredSize, read it in buffer
			//		ipStream.read
			//		callback.message(seqNo,prototype);
		}
		
		
		function onCompleteData(event:Event):void {
			trace("Complete Data");
			if (requiredSize == -1) {
				if (inputBuffer.bytesAvailable < 4 ) {
					//				inputBuffer.readBytes(sizeBuffer ,0 , inputBuffer.bytesAvailable);
					return;
				}
				var sizeBuffer:ByteArray = new ByteArray();
				inputBuffer.readBytes(sizeBuffer,0 , 4);	
				var ipStream1:CodedInputStream = CodedInputStream.newInstance(sizeBuffer);
				requiredSize = ipStream1.readRawLittleEndian32();
			}
			if (requiredSize == -1) return;
			//		Alert.show("b length: " + buffer.length  + "\n bytesA: " + bytesAvailable  + "\n Req: " + requiredSize);
			trace( "Req: " + requiredSize + "   buffer_length: " + buffer.length  + "     bytes_Available: " + inputBuffer.bytesAvailable  + "     bytes_Position: " + inputBuffer.position);
			//if ((buffer.length + inputBuffer.bytesAvailable) < requiredSize) {
			if ((requiredSize - buffer.length) > inputBuffer.bytesAvailable) {
				inputBuffer.readBytes(buffer,buffer.length , inputBuffer.bytesAvailable);
				return;
			} else {
				inputBuffer.readBytes(buffer, buffer.length , (requiredSize - buffer.length));
			}
			trace("Before processing: " + buffer.length);
			var ipStream:CodedInputStream = CodedInputStream.newInstance(buffer);
			var seqNo:BigInteger = ipStream.readInt64();
			var messageType:String = ipStream.readString();
			var prototype:Message = Message(expectedMessages.getItem(messageType));
			if (prototype != null) {
				var  b:Builder = prototype.newBuilderForType();
				ipStream.readMessage(b);
				try {
					callback.message(seqNo, b.build());
				} catch(e:Error) {
					//TODO show in UI?
					//trace(e);
					//throw new Error(e);
					Logger.log("Application Error: " + e + "\n Please restart the application. Notify your administrator with the error details.");
				}
			}
			requiredSize = -1;
			lastReadByte = -1;
			buffer = new ByteArray();
			if (inputBuffer.bytesAvailable > 0) {
				onCompleteData(null);
			}			
		}
		
		//	private static function popFromBuffer(buffer:ByteArray, size:int):ByteArray {
		//		if (buffer.position < size) {
		//			return null;
		//		} else {
		//			ByteArray result = new ByteArray();
		//			buffer.flip();
		//			buffer.get(result);
		//			buffer.compact();
		//			return result;
		//		}
		//	}
		
		public function sendMessage(seqNo:BigInteger, msg:Message, expectedResp:Message):void {
			expectedMessages.addItem(expectedResp.getDescriptorName(),expectedResp);
			send(seqNo,msg);
		}
		
		private function send(seqNo:BigInteger, msg:Message):void {
			var messageType:String = msg.getDescriptorName();
			var size:int = CodedOutputStream.computeInt64SizeNoTag(seqNo)
				+ CodedOutputStream.computeStringSizeNoTag(messageType)
				+ CodedOutputStream.computeMessageSizeNoTag(msg);
			outputStream.writeRawLittleEndian32(size); // i.e., not including itself
			outputStream.writeInt64NoTag(seqNo);
			outputStream.writeStringNoTag(messageType);
			outputStream.writeMessageNoTag(msg);
			outputStream.flush();
			flush();
		}
	}
}